package managers;

import java.io.IOException;
import java.util.ArrayList;

import network.managers.ObserverManager;
import network.server.ModeHandlerThread;
import managers.strategy.HallCallStrategy;
import messages.elevator.HallCallAssignmentMessage;
import messages.elevator.HallCallRevokeMessage;
import state.Globals;
import state.group.GroupDispatcherState;
import state.group.HallCall;
import state.group.events.HallCallEvent;

/**
 * This class is responsible for monitoring the state
 * of execution of various hall calls.  It contains the
 * methods to assign hall calls, and to reassign hall calls
 * if necessary.
 * 
 * Singleton class
 * 
 * @author Ian Doerr
 */
public class HallCallManager implements Runnable {
    
    private static HallCallManager manager;
    private static Thread runThread;
    
    /**
     * Return the current instance of the Hall Call Manager
     * 
     * @return
     */
    public static HallCallManager getInstance()
    {
        return manager;
    }
    
    /**
     * Initialize the Hall Call Manager with the given strategy.
     * 
     * @param strategy
     */
    public static void init(HallCallStrategy strategy)
    {
        manager = new HallCallManager(strategy);
        runThread = new Thread(manager, "Hall Call Manager Thread");
        runThread.start();
    }
    
    /**
     * Notify the manager thread that it needs to shut down.
     * Return after the thread has successfully shut down.
     */
    public static void shutdown()
    {
        manager.shouldContinue = false;
        
        //the manager may be sleeping, so it is necessary to wake it up.
        synchronized(manager)
        {
            manager.notify();
        }
        
        try {
			runThread.join();
		} catch (InterruptedException e) {
			//NOOP
		}
    }
    
    private boolean shouldContinue = true;
    private HallCallStrategy strategy;
    
    /**
     * Disallow instantiation outside of this class.
     * 
     * @param strategy
     */
    private HallCallManager(HallCallStrategy strategy)
    {
        this.strategy = strategy;
    }
    
    /**
     * This method performs the basic hall call registration operation.
     * This can be performed on any hall call that is either not registered
     * or unassigned
     * 
     * @param call the Hall Call to be registered
     * @return success
     */
    public boolean registerHallCall(HallCall call)
    {
        GroupDispatcherState state = ElevatorManager.getInstance().getDispatcherState();
        
        //make sure that the Hall Call hasn't been assigned, or doesn't exist
        synchronized (state.hallCallMappings)
        {
            Byte oldId = state.hallCallMappings.get(call);
            
            if (oldId == null)
            {
                state.hallCallMappings.put(call, (byte) 0);
            }
            else if (oldId != 0)
            {
                return true;
            }
        }
        
        //get the hall call elevator id from the strategy
        Byte elevatorId = strategy.assignHallCall(call);
        
        if (elevatorId == 0)
        {
            return false;
        }
        
        synchronized(state.hallCallMappings)
        {
            state.hallCallMappings.put(call, elevatorId);
        }
        
        //notify the observers about the hall call
        HallCallEvent event = new HallCallEvent(call, true);
        ObserverManager.getInstance().postEvent(event);

        //send the assignment message to the relevant elevator
        HallCallAssignmentMessage mess = new HallCallAssignmentMessage(call);
        
        ModeHandlerThread thread = null;
        
        synchronized (state.elevatorIPs)
        {
            thread = ElevatorManager.getInstance().getElevatorThreads().get(elevatorId);
        }
        
        if (thread == null)
        {
            Globals.getLogger().error("Could not find the thread that is supposed to be connected to the elevator");
        }
        else
        {
        	
        	if (Globals.isDebug())
        	{
        		Globals.getLogger().log("Hall Call Manager sending out message to elevator " + elevatorId + ": " + mess);
        	}
        	
            try
            {
                thread.getRemoteSocket().getOutputStream().write(mess.toByteArray());
            }
            catch (IOException e)
            {
                Globals.getLogger().error("Error occurred while sending out the hall call assignment message", e);
            }
        }
        
        return true;
    }
    
    /**
     * Resolve the hall call, and revoke it if necessary
     * 
     * @param call the hall call to be resolved
     * @param shouldRevoke - whether the hall call needs to be revoked from the elevator
     * @return success
     */
    public boolean resolveHallCall(HallCall call)
    {
        GroupDispatcherState state = ElevatorManager.getInstance().getDispatcherState();
        
        //remove the hall call from the list
        Byte oldValue = 0;
        synchronized (state.hallCallMappings)
        {
        	
        	if (call.getDirection() == 2)
        	{
        		for (HallCall c : state.hallCallMappings.keySet())
                {
                	if (c.getFloor() == call.getFloor())
                	{
                		oldValue = state.hallCallMappings.remove(c);
                		call = c;
                		break;
                	}
                }
        	}
        	else
        	{
        		if (state.hallCallMappings.containsKey(call))
                {
                	oldValue = state.hallCallMappings.remove(call);
                }
        	}
        
        }
        
        if (oldValue == 0)
        {
        	return false;
        }
        
        //notify the observers that this hall call has been resolved.
        HallCallEvent event = new HallCallEvent(call, false);
        ObserverManager.getInstance().postEvent(event);
        
        //send the revoke message
        HallCallRevokeMessage mess = new HallCallRevokeMessage(call);
        
        ModeHandlerThread thread = null;
        
        synchronized (state.elevatorIPs)
        {
            thread = ElevatorManager.getInstance().getElevatorThreads().get(oldValue);
        }
        
        if (thread == null)
        {
            Globals.getLogger().error("Could not find the thread that is supposed to be connected to the elevator");
        }
        else
        {
        	if (Globals.isDebug())
        	{
        		Globals.getLogger().log("Hall Call Manager sending out message to elevator " + oldValue + ": " + mess);
        	}
        	
            try
            {
                thread.getRemoteSocket().getOutputStream().write(mess.toByteArray());
            }
            catch (IOException e)
            {
                Globals.getLogger().error("Error occurred while sending out the hall call assignment message", e);
            }
        }
        
        //free up any resources that the strategy had related to the hall call.
        strategy.resolveHallCall(call);
        
        return true;
    }
    
    /**
     * This is the run method of the Hall Call Manager
     */
    @Override
    public void run()
    {
        GroupDispatcherState state = ElevatorManager.getInstance().getDispatcherState();
        
        while (shouldContinue)
        {
            //wait to be notified to check the hall call assignments
            try
            {
                synchronized(this)
                {
                    wait();
                }
            }
            catch (InterruptedException e)
            {
                //NOOP
            }
            
            if (!shouldContinue)
            {
                break;
            }
            
            //get the list of hall calls that need to be assigned
            ArrayList<HallCall> needToAssign = new ArrayList<HallCall>();
            
            synchronized (state.elevatorData)
            {
                synchronized (state.hallCallMappings)
                {
                    for (HallCall c : state.hallCallMappings.keySet())
                    {
                        Byte id = state.hallCallMappings.get(c);
                        
                        //if this hall call is unassigned
                        if (id == 0)
                        {
                            needToAssign.add(c);
                        }
                        //if the related elevator has disconnected
                        else if (!state.elevatorData.containsKey(id))
                        {
                            needToAssign.add(c);
                        }
                    }
                }
            }
            
            //register all of the necessary hall calls
            for (HallCall c : needToAssign)
            {
                registerHallCall(c);
            }
            
            //identify hall calls that will need to be re-assigned based on the strategy
            strategy.verifyHallCalls();
            
        }
    }
    
}
